Skip to content

feat: API monitoring#49

Open
wylited wants to merge 6 commits intomainfrom
feat/api-monitoring
Open

feat: API monitoring#49
wylited wants to merge 6 commits intomainfrom
feat/api-monitoring

Conversation

@wylited
Copy link

@wylited wylited commented Jan 20, 2026

How this monitoring system will work is that each API service will expose a fastify-metrics endpoint at /metrics, which requires a prometheus key to access.
this is a Prometheus scrapable endpoint for our monitoring platform.

Furthermore, if provided, it will fastify will log automatically to a Loki logging server using pino-loki.

The rest of the setup for monitoring will be done on the usthing server.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds API monitoring capabilities to the template API service by integrating Prometheus metrics collection via fastify-metrics and optional Loki logging via pino-loki. The metrics endpoint can be optionally protected with a bearer token for security.

Changes:

  • Added fastify-metrics and pino-loki dependencies for monitoring and logging
  • Implemented configurable Loki transport for centralized logging
  • Created /metrics endpoint with optional authentication via PROMETHEUS_KEY
  • Updated test helper to support passing custom options for testing different configurations

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
yarn.lock Added dependencies for fastify-metrics, pino-loki, prom-client, and related packages
package.json Added fastify-metrics and pino-loki to dependencies
src/app.ts Implemented Loki transport configuration, metrics endpoint with optional authentication
test/helper.ts Modified build function to accept optional AppOptions for flexible testing
test/routes/metrics.test.ts Added comprehensive tests for metrics endpoint with and without authentication

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

});

// Register Metrics
const metricsEndpoint: RouteOptions | string | null = opts.prometheusKey
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metricsEndpoint variable is typed as 'RouteOptions | string | null' but is only assigned either a RouteOptions object or a string. The 'null' type is never used and should be removed from the type annotation to accurately reflect the actual possible values.

Suggested change
const metricsEndpoint: RouteOptions | string | null = opts.prometheusKey
const metricsEndpoint: RouteOptions | string = opts.prometheusKey

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since opts.prometheusKey, is nullable, so is the route.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that the entire statement is always non-null:

  const metricsEndpoint: RouteOptions | string | null = opts.prometheusKey
    ? {
        url: "/metrics",
        method: "GET",
        handler: async () => {}, // Overridden by fastify-metrics
        onRequest: async (request: FastifyRequest, reply: FastifyReply) => {
          if (
            request.headers.authorization !== `Bearer ${opts.prometheusKey}`
          ) {
            reply.code(401).send({ status: "error", message: "Unauthorized" });
            return;
          }
        },
      }
    : "/metrics";

If opts.prometheusKey is null then it goes to the falsy branch and evaluates to "/metrics". Maybe it's not what you intended?

Comment on lines +93 to +115
if (existingLogger && typeof existingLogger === "object") {
const loggerOptions = existingLogger as { transport?: unknown };
const existingTransport = loggerOptions.transport;

let mergedTransport: unknown;
if (Array.isArray(existingTransport)) {
mergedTransport = [...existingTransport, lokiTransport];
} else if (existingTransport) {
mergedTransport = [existingTransport, lokiTransport];
} else {
mergedTransport = lokiTransport;
}

options.logger = {
...(existingLogger as object),
transport: mergedTransport,
} as Exclude<FastifyServerOptions["logger"], boolean | undefined>;
} else {
options.logger = {
level: "info",
transport: lokiTransport,
};
}
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Loki transport configuration modifies the options object at module level before the app is created. If the logger option is a boolean (true/false) rather than an object, the code in the else block (lines 111-114) will override it, potentially causing unexpected behavior. The condition on line 93 should also check if existingLogger is not a boolean value to properly handle all logger configuration types.

Copilot uses AI. Check for mistakes.
Copy link
Member

@polyipseity polyipseity Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the above comment, you can proceed as follows if the type is a boolean:

  • If the boolean is true, you existing logic should already work properly.
  • If false then you might not want to enable loki.

@polyipseity polyipseity self-requested a review February 17, 2026 12:43
Copy link
Member

@polyipseity polyipseity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed! Once you address the two minor issues by yourself, you can merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments